#!/usr/bin/python2.5
#
# (c) 2009 - Chip Turner
#
# Apache License, blah blah, it's a few hundred lines of python.

import logging
import os
import sqlite3
import tempfile
import time
import urllib
import urllib2
from xml.etree import ElementTree

USER_AGENT = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1'
CACHE_EXPIRATION_SECONDS = 300

class Throttle(object):
  max_req_per_second = 1.0/1.4
  initial_burst_max = 10

  def __init__(self):
    self._last_request = 0
    self._num_requests = 0

  def Check(self):
    delta = time.time() - self._last_request
    delay = 1/self.max_req_per_second - delta
    if delay > 0 and self._num_requests > self.initial_burst_max:
      logging.info("Throttling http request for %.2f seconds..." % delay)
      time.sleep(delay)
    self._num_requests += 1
    self._last_request = time.time()

def InitLogging(level=logging.INFO):
  f = "%(asctime)s %(levelname)-8s %(message)s"
  logging.basicConfig(level=level, format=f)

class ArmoryUrl(object):
  ACHIEVEMENT_BASE_URL = 'character-achievements.xml'
  STATISTICS_BASE_URL = 'character-statistics.xml'
  GUILD_BASE_URL = 'guild-info.xml'
  CHARACTER_BASE_URL = 'character-sheet.xml'
  ITEM_INFO_BASE_URL = "item-info.xml"
  TOOLTIP_BASE_URL = "item-tooltip.xml"
  REPUTATION_BASE_URL = "character-reputation.xml"

  def __init__(self, region):
    if region == "eu":
      self.domain = "eu.wowarmory.com"
      self.lang = "en_us"   # TODO: hmm probably should be something else
    elif region == "us":
      self.domain = "us.wowarmory.com"
      self.lang = "en_us"
    else:
      raise "Invalud ArmoryUrl region, must be us or eu: %s" % region

  def MakeUrl(self, url_type, **args):
    attr = url_type + "_BASE_URL"
    if not hasattr(self, attr):
      raise "Invalid URL: %s" % url_type

    str_args = ""
    if args:
      str_args = "?" + "&".join("%s=%s" % (k, v) for k, v in args.items())

    return "http://%s/%s%s" % (self.domain, getattr(self, attr), str_args)

class Achievement(object):
  def __init__(self, name, id_, completed=None):
    self.name = name
    self.achievement_id = id_
    self.completed = completed
    self.criteria = dict()

  def AddCriteria(self, name, completed_date):
    self.criteria[name] = completed_date

  def GetCriteria(self):
    ret = list()
    for name, completed in self.criteria.items():
      ret.append((name, completed))

    ret.sort(key=lambda a: (a[1], a[0]), reverse=True)
    return ret

class Reputation(object):
  REPUTATION_LEVELS = [
    ( -42000,  -6000, "Hated"),
    (  -6000,  -3000, "Hostile"),
    (  -3000,      0, "Unfriendly"),
    (      0,   3000, "Neutral"),
    (   3000,   9000, "Friendly"),
    (   9000,  21000, "Honored"),
    (  21000,  42000, "Revered"),
    (  42000,  43000, "Exalted"),
    ]

  def __init__(self, key, name, value):
    self.name = name
    self.key = key
    self.absolute_value = value
    self.value_string = None
    i = 0
    for lower, upper, string in self.REPUTATION_LEVELS:
      if lower <= value <= upper:
        self.relative_value = value - lower
        self.next_level = upper - lower
        self.pretty_reputation = string
        self.position = i
        break
      i += 1
    assert self.pretty_reputation is not None, "bad rep: %d" % value

class EquippedItem(object):
  def __init__(self, id_, slot, enchant_id):
    self.item_id = id_
    self.slot = slot
    self.enchant_id = enchant_id
    self._gem_ids = list()

  def AddGem(self, gem_id):
    self._gem_ids.append(gem_id)

  def Gems(self):
    return list(self._gem_ids)

class Character(object):
  def __init__(self, realm, name):
    self.realm = realm
    self.name = name
    self._gear = list()
    self._professions = set()
    self._specs = list()
    self._active_spec = None

  def AddProfession(self, name):
    self._professions.add(name)

  def AddItem(self, item):
    self._gear.append(item)

  def Gear(self):
    return list(self._gear)

  def Professions(self):
    return sorted(self._professions)

  def AddSpec(self, name, active, *trees):
    trees = map(int, trees)
    active = int(active)

    if active:
      self._specs.insert(0, [name])
      self._specs[0].extend(trees)
    else:
      self._specs.append([name])
      self._specs[-1].extend(trees)

  def GetSpecs(self):
    return self._specs


class ArmoryInterface(object):
  def __init__(self, region):
    self.url = ArmoryUrl(region)
    self._cached_string_data = None
    self.throttle = Throttle()
    
  def _GetCachedUrl(self, url):
    conn = self._GetCacheConnection()
    try:
      c = conn.execute("SELECT created, contents FROM url_cache WHERE url = ?", (url,))
      rows = list(c)
      if len(rows) == 1:
        return rows[0][0], rows[0][1]
      else:
        return 0, None
    finally:
      conn.close()

  def _UpdateCache(self, url, contents):
    conn = self._GetCacheConnection()
    try:
      c = conn.execute("REPLACE INTO url_cache (created, url, contents) VALUES (?, ?, ?)", (time.time(), url, contents))
      conn.commit()
    finally:
      conn.close()

  def _FetchArmoryUrl(self, url):
    logging.debug("Fetching %s", url)
    while 1:
      try:
        created, contents = self._GetCachedUrl(url)
        if time.time() - created < CACHE_EXPIRATION_SECONDS:
          return contents
        self.throttle.Check()
        request = urllib2.Request(url)
        request.add_header("User-agent", USER_AGENT)
        ret = urllib2.urlopen(request).read()
        self._UpdateCache(url, ret)
        return ret
      except urllib2.HTTPError, he:
        if he.code == 503:
          logging.debug("Received 503 error, pausing...")
          time.sleep(5)
        else:
          raise

  def ParseStats(self, realm, name, category):
    url = self.url.MakeUrl("STATS", r=realm, n=urllib.quote(name), c=int(category))
    overview = self._FetchArmoryUrl(url)
    ret = dict()
    tree = ElementTree.fromstring(overview)
    category = None
    for elem in tree.getiterator():
      if elem.tag == 'category':
        category = elem.get('name')
        ret.setdefault(category, list())
      elif elem.tag == 'statistic':
        assert category != None
        if "name" in elem.attrib and "quantity" in elem.attrib:
          quantity = elem.get('quantity')
          if quantity == '--': quantity = 0
          ret[category].append((elem.get('name'), int(quantity)))

    return ret

  def ParseReputation(self, realm, name):
    url = self.url.MakeUrl("REPUTATION", r=realm, n=urllib.quote(name))
    overview = self._FetchArmoryUrl(url)
    ret = list()
    tree = ElementTree.fromstring(overview)
    for elem in tree.getiterator():
      if elem.tag == 'faction' and elem.get('header') == None:
        rep = Reputation(elem.get('key'), elem.get('name'), int(elem.get('reputation')))
        ret.append(rep)

    return ret

  def ParseCategories(self, realm, name):
    url = self.url.MakeUrl("ACHIEVEMENT", r=realm, n=urllib.quote(name))
    overview = self._FetchArmoryUrl(url)
    ret = list()
    tree = ElementTree.fromstring(overview)
    for elem in tree.getiterator("category"):
      if "id" in elem.attrib:
        ret.append((elem.get('name'), int(elem.get('id') or 0)))

    ret.sort()
    return ret

  def ParseAchievements(self, realm, name, category):
    url = self.url.MakeUrl("ACHIEVEMENT", r=realm, n=urllib.quote(name), c=int(category))
    cat_details = self._FetchArmoryUrl(url)
    ret = list()
    tree = ElementTree.fromstring(cat_details)
    for elem in tree.getiterator():
      if elem.tag == 'achievement':
        ret.append(Achievement(elem.get('title'), elem.get('id'), elem.get('dateCompleted')))
      elif elem.tag == 'criteria':
        ret[-1].AddCriteria(elem.get('name'), elem.get('date'))

    ret.sort(key=lambda a: (a.completed, a.name), reverse=True)
    return ret

  def ParseCharacter(self, realm, name):
    url = self.url.MakeUrl("CHARACTER", r=realm, n=name)
    char_details = self._FetchArmoryUrl(url)
    ret = list()
    tree = ElementTree.fromstring(char_details)
    c = Character(realm, name)
    for elem in tree.getiterator():
      if elem.tag == 'item':
        g = EquippedItem(int(elem.get("id")), int(elem.get("slot")), int(elem.get("permanentenchant")))
        for n in range(5):
          gem_id = elem.get("gem%dId" % n)
          if gem_id is not None:
            g.AddGem(int(gem_id))
        c.AddItem(g)
      elif elem.tag == 'skill':
        c.AddProfession(elem.get("name"))
      elif elem.tag == 'talentSpec':
        c.AddSpec(elem.get("prim"), elem.get("active", 0),
                  elem.get("treeOne"), elem.get("treeTwo"), elem.get("treeThree"))

    return c

  def ParseRoster(self, realm, name):
    url = self.url.MakeUrl("GUILD", r=realm, n=urllib.quote(name))
    roster = self._FetchArmoryUrl(url)
    ret = list()
    tree = ElementTree.fromstring(roster)
    for elem in tree.getiterator("character"):
      row = list()
      for field in "name level raceId classId rank".split():
        if field in ("level", "rank", "raceId", "classId"):
          row.append(int(elem.get(field)))
        else:
          row.append(elem.get(field).encode('utf-8'))  # we want raw bytes
      ret.append(row)

    ret.sort(key=lambda a: (a[-1], a[0]))
    return ret

  def ParseItem(self, item_id):
    url = self.url.MakeUrl("ITEM_INFO", i=item_id)
    info = self._FetchArmoryUrl(url)
    ret = list()
    tree = ElementTree.fromstring(info)
    for elem in tree.getiterator("item"):
      return dict(name=elem.get("name"), id=int(elem.get("id")), ilvl=int(elem.get('level')))
    return None

  def ParseToolTip(self, realm, name, slot_id, item_id):
    url = self.url.MakeUrl("TOOLTIP", r=realm, n=name, s=slot_id, i=item_id)
    info = self._FetchArmoryUrl(url)
    ret = list()
    tree = ElementTree.fromstring(info)
    for elem in tree.getiterator("enchant"):
      return dict(enchant=elem.text)
    return None

  def _GetCacheConnection(self):
    filename = os.path.join(tempfile.gettempdir(), "armory_lib_cache.db")
    conn = sqlite3.connect(filename)
    conn.text_factory = str
    c = conn.execute("SELECT name FROM sqlite_master")
    tables_seen = set(row[0] for row in c)
    if "items" not in tables_seen:
      conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, ilvl INTEGER)")
      conn.commit()
    if "url_cache" not in tables_seen:
      conn.execute("CREATE TABLE url_cache (url BLOB PRIMARY KEY, created INTEGER, contents BLOB)")
      conn.commit()

    return conn
      
  def ItemInfo(self, item_id):
    conn = self._GetCacheConnection()
    try:
      c = conn.execute("SELECT name, ilvl FROM items WHERE id = ?", (item_id,))
      rows = list(c)
      if len(rows) == 1:
        return dict(id=item_id, name=rows[0][0], ilvl=rows[0][1])
      elif len(rows) == 0:
        item = self.ParseItem(item_id)
        if not item:
          return dict(name="(missing item:%d)" % item_id, id=item_id, ilvl=0)
        conn.execute("INSERT INTO items VALUES (?, ?, ?)", (item["id"], item["name"], item["ilvl"]))
        conn.commit()
        return item
    finally:
      conn.close()

  enchant_cache = None
  def EnchantInfo(self, realm, name, slot_id, item_id, enchant_id):
    if not self.enchant_cache:
      self.enchant_cache = dict()
      enchant_file = os.path.join(os.path.dirname(__file__), "permanentenchant.xml")
      tree = ElementTree.fromstring(open(enchant_file).read())
      self.enchant_cache = dict()
      for elem in tree.getiterator("permanentenchant"):
        e = dict(name=elem.get("name"), id=int(elem.get("id")))
        self.enchant_cache[e["id"]] = e

    ret = self.enchant_cache.get(enchant_id, None)
    if not ret:
      tip = self.ParseToolTip(realm, name, slot_id, item_id)
      ret = self.enchant_cache[enchant_id] = dict(enchant_id=enchant_id, name=tip["enchant"])
    return ret

race_cache = "Human,Orc,Dwarf,Night Elf,Undead,Tauren,Gnome,Troll,UnknownRace,Blood Elf,Draenei".split(",")
class_cache = "Warrior,Paladin,Hunter,Rogue,Priest,Death Knight,Shaman,Mage,Warlock,UnknownClass,Druid".split(",")
def ReadableRaceAndClass(race_id, class_id):
  r, c = race_id, class_id
  if race_id <= len(race_cache):
    r = race_cache[race_id-1]
  if class_id <= len(class_cache):
    c = class_cache[class_id-1]

  return r, c
